Перейти к основному содержимому

4.06. Вызовы и иерархия

Разработчику Архитектору Инженеру

Вызовы и иерархия

Цепочки вызовов — кто кого вызвал

Цепочка вызовов — это последовательность методов или функций, которые вызывают друг друга в процессе выполнения программы.

Каждый вызов функции добавляет новый фрейм в стек вызовов. Стек вызовов отслеживает:

  • какая функция вызвала текущую
  • какие аргументы были переданы
  • куда вернуться после завершения

Пример цепочки вызовов в C#:

public class OrderProcessor
{
public void ProcessOrder(Order order)
{
ValidateOrder(order); // Вызов 1
CalculateTotal(order); // Вызов 2
SaveOrder(order); // Вызов 3
SendConfirmation(order); // Вызов 4
}

private void ValidateOrder(Order order)
{
CheckInventory(order); // Вызов 1.1
ValidateCustomer(order); // Вызов 1.2
}

private void CheckInventory(Order order)
{
// Логика проверки наличия товаров
}
}

Стек вызовов при выполнении CheckInventory:

CheckInventory(order)

ValidateOrder(order)

ProcessOrder(order)

Main()

Пример на Python:

def process_order(order):
validate_order(order) # Вызов 1
calculate_total(order) # Вызов 2
save_order(order) # Вызов 3

def validate_order(order):
check_inventory(order) # Вызов 1.1
validate_customer(order) # Вызов 1.2

def check_inventory(order):
# Логика проверки наличия товаров
pass

Глубина стека
Глубина стека вызовов ограничена. Превышение лимита приводит к ошибке переполнения стека (StackOverflowError в Java, StackOverflowException в C#). Типичный лимит — от нескольких тысяч до десятков тысяч вызовов в зависимости от платформы и настроек.


Рекурсия — плюсы, минусы, переполнение стека

Рекурсия — это техника, при которой функция вызывает саму себя для решения задачи.

Рекурсивные алгоритмы подходят для задач с естественной рекурсивной структурой:

  • обход деревьев и графов
  • математические последовательности
  • разбор вложенных структур данных
  • комбинаторные задачи

Пример факториала:

public int Factorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}

// Вызов Factorial(5):
// Factorial(5) = 5 * Factorial(4)
// = 5 * 4 * Factorial(3)
// = 5 * 4 * 3 * Factorial(2)
// = 5 * 4 * 3 * 2 * Factorial(1)
// = 5 * 4 * 3 * 2 * 1 = 120
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)

# Вызов factorial(5) строит стек из 5 фреймов
public int factorial(int n) {
if (n <= 1)
return 1;
return n * factorial(n - 1);
}

Преимущества рекурсии:

  • естественное выражение рекурсивных задач
  • компактный и читаемый код для определённых алгоритмов
  • упрощение реализации обхода древовидных структур

Недостатки рекурсии:

  • потребление памяти стека пропорционально глубине рекурсии
  • риск переполнения стека при большой глубине
  • накладные расходы на создание фреймов стека
  • сложность отладки по сравнению с итеративными решениями

Пример переполнения стека:

public void InfiniteRecursion()
{
InfiniteRecursion(); // Бесконечная рекурсия без условия выхода
}

// При вызове: StackOverflowException

Итеративная альтернатива факториала:

public int FactorialIterative(int n)
{
int result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
}

Хвостовая рекурсия (концептуально)

Хвостовая рекурсия — это форма рекурсии, при которой рекурсивный вызов является последней операцией в функции.

Особенность хвостовой рекурсии — компилятор или интерпретатор может оптимизировать её в итеративный цикл, избегая роста стека вызовов.

Пример хвостовой рекурсии на факториале:

public int FactorialTail(int n, int accumulator = 1)
{
if (n <= 1)
return accumulator;
return FactorialTail(n - 1, n * accumulator);
// Рекурсивный вызов — последняя операция
}
def factorial_tail(n, accumulator=1):
if n <= 1:
return accumulator
return factorial_tail(n - 1, n * accumulator)

Сравнение стека вызовов:

Обычная рекурсия (Factorial(5)):

Factorial(5) ожидает результат от Factorial(4)
Factorial(4) ожидает результат от Factorial(3)
Factorial(3) ожидает результат от Factorial(2)
Factorial(2) ожидает результат от Factorial(1)
Factorial(1) возвращает 1
Factorial(2) вычисляет 2 * 1 и возвращает 2
Factorial(3) вычисляет 3 * 2 и возвращает 6
Factorial(4) вычисляет 4 * 6 и возвращает 24
Factorial(5) вычисляет 5 * 24 и возвращает 120

Хвостовая рекурсия (FactorialTail(5, 1)):

FactorialTail(5, 1) → FactorialTail(4, 5)
FactorialTail(4, 5) → FactorialTail(3, 20)
FactorialTail(3, 20) → FactorialTail(2, 60)
FactorialTail(2, 60) → FactorialTail(1, 120)
FactorialTail(1, 120) возвращает 120

При оптимизации хвостовой рекурсии стек не растёт — каждый вызов заменяется переходом с новыми аргументами.

Поддержка оптимизации хвостовой рекурсии:

  • F#, Scala, Erlang — полная поддержка на уровне языка
  • JavaScript (ES6) — спецификация требует поддержки, но реализация в браузерах ограничена
  • C# — нет встроенной оптимизации, но возможна ручная трансформация в цикл
  • Python — нет оптимизации, рекомендуются итеративные решения
  • Java — нет оптимизации на уровне JVM

Инструменты — дерево вызовов, flame graph, профилирование

Дерево вызовов — визуальное представление иерархии вызовов функций в программе.

Пример дерева вызовов для обработки заказа:

ProcessOrder()
├─ ValidateOrder()
│ ├─ CheckInventory()
│ │ └─ QueryDatabase()
│ └─ ValidateCustomer()
│ └─ CallExternalAPI()
├─ CalculateTotal()
│ └─ ApplyDiscounts()
└─ SaveOrder()
└─ WriteToDatabase()

Flame graph — тепловая карта вызовов, где:

  • ширина блока пропорциональна времени выполнения
  • вертикальная позиция показывает глубину стека
  • цвет может обозначать модуль или тип операции

Пример интерпретации flame graph:

[████████████████████████████████] ProcessOrder()      100ms
[██████████] ValidateOrder() 40ms
[████] CheckInventory() 15ms
[██] QueryDatabase() 8ms
[████] ValidateCustomer() 15ms
[███] CallExternalAPI() 12ms
[████████] CalculateTotal() 30ms
[███] ApplyDiscounts() 12ms
[████] SaveOrder() 15ms
[███] WriteToDatabase() 12ms

Инструменты для анализа вызовов:

ИнструментПлатформаОсобенности
Visual Studio Profiler.NETИнтеграция с IDE, call tree view
dotTrace.NETFlame graphs, timeline analysis
YourKitJavaCPU/memory profiling, call chains
Async ProfilerJVMLow-overhead, flame graphs
py-spyPythonSampling profiler, flame graphs без модификации кода
Chrome DevToolsJavaScriptPerformance tab, call tree
PerfLinuxСистемный профилировщик, поддержка flame graphs

Пример использования профилировщика в .NET:

// Запуск профилирования через командную строку
dotnet trace collect --profile cpu-sampling -o trace.nettrace -- myapp.dll

// Анализ результата в SpeedScope или PerfView

Пример использования py-spy для Python:

# Запуск профилирования без изменения кода
py-spy record -o profile.svg -- python myapp.py

# Режим top для реального времени
py-spy top --pid 12345

Обратные вызовы

Обратный вызов (callback) — это функция, передаваемая как аргумент другой функции и вызываемая по завершении определённого события или операции.

Обратные вызовы применяются для:

  • асинхронных операций
  • обработки событий
  • реализации стратегий и политик
  • расширения поведения без изменения исходного кода

Пример обратного вызова в JavaScript:

// Синхронный обратный вызов
function processData(data, callback) {
const result = data.map(item => item * 2);
callback(result);
}

processData([1, 2, 3], (result) => {
console.log(result); // [2, 4, 6]
});

// Асинхронный обратный вызов
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(response => response.json())
.then(data => onSuccess(data))
.catch(error => onError(error));
}

fetchData(
'https://api.example.com/data',
(data) => console.log('Данные получены:', data),
(error) => console.error('Ошибка:', error)
);

Пример на C# с делегатами:

public delegate void DataProcessedHandler(int[] result);

public class DataProcessor
{
public void Process(int[] data, DataProcessedHandler callback)
{
var result = data.Select(x => x * 2).ToArray();
callback(result);
}
}

// Использование
var processor = new DataProcessor();
processor.Process(new[] { 1, 2, 3 }, result => {
Console.WriteLine(string.Join(", ", result)); // 2, 4, 6
});

Пример на Python с функциями высшего порядка:

def process_data(data, callback):
result = [x * 2 for x in data]
callback(result)

def print_result(result):
print(result)

process_data([1, 2, 3], print_result) # [2, 4, 6]

# Использование лямбды
process_data([1, 2, 3], lambda r: print(f"Результат: {r}"))

Проблемы обратных вызовов:

  • callback hell — вложенность обратных вызовов затрудняет чтение кода
  • сложность обработки ошибок в асинхронных цепочках
  • трудности с отменой операций
  • потеря контекста выполнения

Современные альтернативы обратным вызовам:

  • Промисы (Promises) — цепочки асинхронных операций
  • Асинхронные функции (async/await) — синхронный стиль для асинхронного кода
  • Реактивные расширения (Rx) — потоки событий с операторами трансформации